#include <QGraphicsEllipseItem>
#include <QyGLInputController.h>
#include <QyGLScene.h>
#include <QyGLViewWidget.h>
#include <QyTexturePool.h>
#include "QpwGeometryEdit.h"

/**************************************************************************************************/

inline QPointF operator / ( const QPointF & p1, const QPointF & p2 ) {

    return QPointF( p1.x() / p2.x(), p1.y() / p2.y() );
}

/**************************************************************************************************/

class GeoChanger {
public:
    QpwWorld::Geometry * geo;
    QpwWorld::Geometry * rootGeo;

    void setCenter( const QPointF & center );
    void setRadius( const QPointF & pos );
    void setPos( int index, const QPointF & pos );
    void applyChanges();

private:
    enum ChangeType {
        NoChange, SetCenter, SetRadius, SetPos
    };
    class Change {
    public:
        explicit Change( ChangeType t = NoChange ) : type(t), index(0), change(false) {}

        ChangeType type;
        int index;
        QPointF pos;
        bool change;

    };

    Change posChange,
        centerChange,
        radiusChange;

};

void GeoChanger::setCenter( const QPointF & center ) {

    centerChange.type = SetCenter;
    centerChange.pos = center;
    centerChange.change = true;

}

void GeoChanger::setRadius( const QPointF & pos ) {

    radiusChange.type = SetRadius;
    radiusChange.pos = pos;
    radiusChange.change = true;

}

void GeoChanger::setPos( int index, const QPointF & pos ) {

    posChange.type = SetPos;
    posChange.index = index;
    posChange.pos = pos;
    posChange.change = true;

}

void GeoChanger::applyChanges() {

    if (centerChange.change) {

        geo->center = QyGL::Scene::mapToGL(centerChange.pos).toPointF();

        if (rootGeo)
            geo->center -= rootGeo->center;

        centerChange.change = false;

    }

    if (posChange.change) {

        (*geo)[posChange.index] = QyGL::Scene::mapToGL(posChange.pos).toPointF() - geo->center;

        if (rootGeo)
            (*geo)[posChange.index] -= rootGeo->center;

        posChange.change = false;

    }

    if (radiusChange.change) {

        QPointF center = QyGL::Scene::mapToGL(radiusChange.pos).toPointF();

        if (rootGeo)
            center -= rootGeo->center;

        QPointF offset = center - geo->center;

        qreal radius = sqrt( offset.x() * offset.x() + offset.y() * offset.y() );

        geo->setRadius(radius);
        radiusChange.change = false;

    }

}

/**************************************************************************************************/

class QySceneItemEditItem : public QGraphicsEllipseItem {
public:
    explicit QySceneItemEditItem( QpwWorld::Geometry * geo, QpwWorld::Geometry * rootGeo = 0, bool center, QySceneItemEditItem * parent = 0 )
        : p_geoChanger(&geoChanger), p_index(-1), p_centerEditor(center), circleEdit(true), parentItemEditor(parent), p_rootGeo(rootGeo)
    {

        geoChanger.rootGeo = rootGeo;
        geoChanger.geo = geo;
        init(parent);

    }
    explicit QySceneItemEditItem( QpwWorld::Geometry * geo, QpwWorld::Geometry * rootGeo = 0, int index, QySceneItemEditItem * parent = 0 )
        : p_geoChanger(&geoChanger), p_index(index), p_centerEditor( index < 0 ), circleEdit(false), parentItemEditor(parent), p_rootGeo(rootGeo)
    {

        geoChanger.rootGeo = rootGeo;
        geoChanger.geo = geo;
        init(parent);

    }

    void init( QySceneItemEditItem * parent ) {

        ignoreChange = false;

        if (parent)
            setParentItem(parent);

//         qreal halfSize = size / 2.0;
        setRect( p_centerEditor ? QRectF( -3, -3, 6, 6 ) : QRectF( -5, -5, 10, 10 ) );
        setPen( QPen(Qt::black) );

        if (parent) {
            setBrush( QBrush( p_centerEditor ? Qt::green : Qt::black ) );
            setZValue(1);
        } else {
            setBrush( QBrush( p_centerEditor ? Qt::blue : Qt::black ) );
            setZValue(1);
        }

//         setZValue( p_centerEditor ? 10 : 15 );
        setFlags(QGraphicsItem::ItemIsMovable);
        updatePos();

    }
    qreal normalizeValue( qreal value ) { return value; }
    QPointF absPos() const {

        return parentItemEditor ? parentItemEditor->absPos() + pos() : pos();
    }
    QPointF geoCenter() const {

        return ( p_rootGeo && parentItemEditor )
            ? p_rootGeo->center + p_geoChanger->geo->center
            : p_geoChanger->geo->center;
    }

    void updatePos() {

        QPointF center = geoCenter();

        if (circleEdit) {

            if (p_centerEditor) {

                center = QyGL::Scene::mapFromGL(center);

                if (parentItemEditor) {

                    center -= parentItemEditor->absPos();

                }

                setPos(center);

            } else {

                center.setY( center.y() - p_geoChanger->geo->radius() );

                center = QyGL::Scene::mapFromGL(center);

                if (parentItemEditor) {

                    center -= parentItemEditor->absPos();

                }

                setPos(center);

            }

        } else {

            if (p_centerEditor) {

                center = QyGL::Scene::mapFromGL(center);

                if (parentItemEditor) {

                    center -= parentItemEditor->absPos();

                }

                setPos(center);

            } else {

                center = QyGL::Scene::mapFromGL( center + (*geoChanger.geo)[p_index] );

                if (parentItemEditor) {

                    center -= parentItemEditor->absPos();

                }

                setPos(center);

            }

        }

    }
    void setPos( const QPointF & pos ) {

        ignoreChange = true;
        QGraphicsEllipseItem::setPos(pos);
        ignoreChange = false;

    }

    GeoChanger geoChanger;

private:
    QVariant itemChange( GraphicsItemChange change, const QVariant & value ) {

        QVariant result = QGraphicsItem::itemChange( change, value );

        if (ignoreChange)
            return result;

        if ( change == ItemPositionHasChanged ) {

            if (circleEdit) {

                if (p_centerEditor) {

                    p_geoChanger->setCenter( absPos() );
//                     setToolTip( QString("%1, %2")
//                         .arg( normalizeValue( p_geoChanger->geo->center.x() ) )
//                         .arg( normalizeValue( p_geoChanger->geo->center.y() ) )
//                     );

                } else {

                    p_geoChanger->setRadius( absPos() );
//                     setToolTip( QString("%1").arg( normalizeValue(radius) ) );

                }

            } else {

                if (p_centerEditor) {

                    p_geoChanger->setCenter( absPos() );
//                     setToolTip( QString("%1, %2")
//                         .arg( normalizeValue( p_geoChanger->geo->center.x() ) )
//                         .arg( normalizeValue( p_geoChanger->geo->center.y() ) )
//                     );

                } else {

                    p_geoChanger->setPos( p_index, absPos() );
//                         setToolTip( QString("%1, %2")
//                             .arg( normalizeValue( center.x() ) )
//                             .arg( normalizeValue( center.y() ) )
//                         );

                }

            }

        }

        return result;
    }
    void contextMenuEvent ( QGraphicsSceneContextMenuEvent * event ) {

//         QMenu menu;

//         QAction * rmItemAction = menu.addAction();

//         QAction * execAction = menu.exec();

    }

    GeoChanger * p_geoChanger;
    int p_index;
    bool p_centerEditor;
    bool circleEdit;
    QySceneItemEditItem * parentItemEditor;
    QpwWorld::Geometry * p_rootGeo;
    bool ignoreChange;

};

/**************************************************************************************************/

class QpwGeometryScene : public QyGL::Scene {
public:
    QpwGeometryScene() : p_firstPaint(false), updateItemPos(false), lastWidth(-1), lastHeight(-1)
    {
        setCamOffset( QyMath::Vector( 0, 0, -30 ) );
        adjustCursor = false;
    }
    virtual ~QpwGeometryScene() {}

    QpwWorld::Geometry geometry() const { return p_geometry; }
    QpwWorld::Geometry * geometryPointer() { return &p_geometry; }

    void setGeometry( const QpwWorld::Geometry & geometry ) {

        p_geometry = geometry;
        p_texCoords = buildTextureCoords(p_geometry);
        updateGeometry();

    }
    void updateGeometry() {

        if ( geoEditItems.count() ) {

            delete geoEditItems.first();
            geoEditItems.clear();

        }

        p_firstPaint = true;

    }

    qreal adjust( qreal value ) const {

        return qreal( qRound(value) );
    }
    QPointF adjust( const QPointF & point ) const {

        return QPointF( adjust( point.x() ), adjust( point.y() ) );
    }
    QyMath::Vector adjust( const QyMath::Vector & vec ) const {

        return QyMath::Vector( adjust(vec.p[0]), adjust(vec.p[1]), adjust(vec.p[2]) );
    }
    QyMath::Vector glCursor() const {

        return adjustCursor ? adjust( QyGL::Scene::glCursor() ) : QyGL::Scene::glCursor();
    }
    QPoint cursorPos() const {

        return p_adjPos;
    }
    bool adjustCursor;

private:
    QySceneItemEditItem * mkGeometryItem( QpwWorld::Geometry * geometry, QpwWorld::Geometry * rootGeo = 0, bool center, QySceneItemEditItem * parent = 0 ) {

        QySceneItemEditItem * item = new QySceneItemEditItem( geometry, rootGeo, center, parent );
        geoEditItems.append(item);
        if (!parent)
            addItem(item);

        return item;
    }
    QySceneItemEditItem * mkGeometryItem( QpwWorld::Geometry * geometry, QpwWorld::Geometry * rootGeo = 0, int index, QySceneItemEditItem * parent = 0 ) {

        QySceneItemEditItem * item = new QySceneItemEditItem( geometry, rootGeo, index, parent );
        geoEditItems.append(item);
        if (!parent)
            addItem(item);

        return item;
    }
    QySceneItemEditItem * mkItems( QpwWorld::Geometry * geometry, QpwWorld::Geometry * rootGeo = 0, QySceneItemEditItem * parent = 0 ) {

        QySceneItemEditItem * rootItem = 0;

        if ( geometry->isCircle() ) {

            rootItem = mkGeometryItem( geometry, rootGeo, true, parent );
            mkGeometryItem( geometry, rootGeo, false, rootItem );

        } else {

            rootItem = mkGeometryItem( geometry, rootGeo, -1, parent );

            for( int i = 0; i< geometry->count(); ++i )
                mkGeometryItem( geometry, rootGeo, i, rootItem );

        }

        return rootItem;
    }

    void initialize() {

        glClearColor( 1, 1, 1, 1 );
        glPolygonOffset( 1.0, 2.0 );
        glPointSize(1);
        glLineWidth(1);
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
        glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
        glEnable(GL_ALPHA_TEST);
        glEnable(GL_BLEND);
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_POLYGON_OFFSET_FILL);

    }
    void drawGrid() {

        glBegin(GL_LINES);
        for ( qreal x = -50; x <= 50; ++ x ) {

            if ( int(x) == 0 )
                glColor3d( 0.0, 0.0, 0.0 );
            else if ( int(x) % 5 )
                glColor3d( 0.7, 0.7, 1 );
            else
                glColor3d( 0.5, 0.5, 1 );

            glVertex3d( x,-50, -0.5 );
            glVertex3d( x, 50, -0.5 );
        }

        for ( qreal y = -50; y <= 50; ++ y ) {

            if ( int(y) == 0 )
                glColor3d( 0.0, 0.0, 0.0 );
            else if ( int(y) % 5 )
                glColor3d( 0.7, 0.7, 1 );
            else
                glColor3d( 0.5, 0.5, 1 );

            glVertex3d( -50, y, 0.0 );
            glVertex3d(  50, y, 0.0 );

        }
        glEnd();


    }
    void paint() {

        p_adjPos = mapFromGL( glCursor() ).toPoint();
        if (p_firstPaint) {

            p_firstPaint = false;

            QySceneItemEditItem * rootItem = mkItems(&p_geometry);

            for ( int i = 0; i < p_geometry.subGeometry.count(); ++i )
                mkItems( &p_geometry.subGeometry[i], &p_geometry, rootItem );

        }

        foreach ( QySceneItemEditItem * geoEdit, geoEditItems ) {

            geoEdit->geoChanger.applyChanges();
//             geoEdit->updatePos();
            if (updateItemPos)
                geoEdit->updatePos();

        }
        updateItemPos = false;

        drawGrid();

        glColor3d( 1.0, 1.0, 1.0 );
        glPushMatrix();

            glEnable(GL_TEXTURE_2D);
//             glTranslated( 0, 0, -0.1 );
            paintGeometry( true, p_geometry, p_texCoords );
            glDisable(GL_TEXTURE_2D);

        glPopMatrix();

        glPushMatrix();

            glTranslated( p_geometry.center.x(), p_geometry.center.y(), 0.1 );
            glColor3d( 0, 0, 0 );
            foreach ( QpwWorld::Geometry geo, p_geometry.subGeometry )
                paintGeometry( false, geo );

        glPopMatrix();

//         glColor3d( 1, 0, 0 );
//         glPointSize(4);
//         glBegin(GL_POINTS);
//         glVertex3dv(glCursor().p);
//         glEnd();

    }
    void resize( qreal width, qreal height ) {

        QyGL::Scene::resize( width, height );

        if ( lastWidth != width || lastHeight != height ) {

            updateItemPos = true;
            lastWidth = width;
            lastHeight = height;

        }

    }
    void paintGeometry( bool fill, const QpwWorld::Geometry & geometry,
        const QPolygonF & texCoords = QPolygonF() ) const
    {

        bool texture = texCoords.count();

        glPushMatrix();
        glTranslated( geometry.center.x(), geometry.center.y(), 0.0 );
        if ( geometry.isCircle() ) {

            //geometry
            static const qreal mpi2 = M_PI * 2.0;
            static const qreal radianPerSector = mpi2 / 8.0;
            const qreal radius = geometry.radius();
            qreal x,
                  y;

            QPolygonF cache;

            glBegin( fill ? GL_POLYGON : GL_LINE_LOOP );
            int i = 0;
            for ( qreal angle = 0; angle <= mpi2; angle += radianPerSector, ++i ) {

                x = sin(angle) * radius;
                y = cos(angle) * radius;

                if (texture)
                    glTexCoord2d( texCoords[i].x(), texCoords[i].y() );
                glVertex3d( x, y, 0 );
                cache.append( QPointF( x, y ) );

            }
            glEnd();

            if (texture)
                glDisable(GL_TEXTURE_2D);

            glColor3d( 0.0, 0.0, 0.0 );
            glBegin(GL_LINE_LOOP);
            foreach ( QPointF p, cache )
                glVertex3d( p.x(), p.y(), 0 );
            glEnd();

        } else {

            int i,
                count = geometry.count();

            glBegin( fill ? GL_POLYGON : GL_LINE_LOOP );
            for ( i = 0; i < count; ++i ) {

                if (texture)
                    glTexCoord2d( texCoords[i].x(), texCoords[i].y() );
                glVertex3d( geometry[i].x(), geometry[i].y(), 0 );

            }
            glEnd();

            glColor3d( 0.0, 0.0, 0.0 );
            glBegin(GL_LINE_LOOP);
            foreach ( QPointF p, geometry )
                glVertex3d( p.x(), p.y(), 0 );
            glEnd();

        }

        glPopMatrix();

    }
    QPolygonF buildTextureCoords( const QpwWorld::Geometry & geometry ) const {

        QPolygonF textCoords;

        if ( geometry.isCircle() ) {

            //geometry
            static const qreal radianPerSector = M_PI * 2.0 / 8.0;
            const qreal radius = geometry.radius();
            const qreal diameter = radius * 2.0;
            textCoords = QPolygonF(8);
            qreal x,
                  y;

            for ( int i = 0; i < 8; ++i ) {

                x = sin( i* radianPerSector ) * radius;
                y = cos( i* radianPerSector ) * radius;

                textCoords.append( QPointF( ( x + radius ) / diameter, ( y + radius ) / diameter ) );

            }

        } else {

            int i,
                count = geometry.count();
            textCoords = QPolygonF(count);
            QRectF br = geometry.boundingRect();
            QPointF currentPos = br.bottomLeft();
            QPointF offset( br.width(), br.height() );

            for ( i = 0; i< count; ++i ) {

                textCoords[i] = ( geometry[i] - currentPos ) / offset;

            }

        }

        return textCoords;
    }

    QpwWorld::Geometry p_geometry;
    QPolygonF p_texCoords;
    bool p_firstPaint;
    QList<QySceneItemEditItem*>  geoEditItems;
    bool updateItemPos;
    qreal lastWidth;
    qreal lastHeight;
    QPoint p_adjPos;

};

/**************************************************************************************************/

class DrawTool : public QyGL::InputController {
public:
    explicit DrawTool( QpwGeometryScene * scene )
        : QyGL::InputController(scene), p_scene(scene), mode(None)
    {

        geometry = p_scene->geometryPointer();

    }

private:
    qreal adjust( qreal value ) {

        return p_scene->adjust(value);
    }
    QPointF adjust( const QPointF & point ) {

        return p_scene->adjust(point);
    }
    enum Mode {
        None, Analyse, DrawCircle, DrawPolygon
    };
    void mousePressd( const QyGL::MouseEvent & event ) {

        bool itemByCursor = p_scene->itemAt( event.pos() );

        if ( pressButtons() == Qt::LeftButton && event.modifiers() & Qt::ControlModifier ) {

            switch (mode) {

            case None:
                mode = itemByCursor ? None : Analyse;
            case Analyse:
                break;

            case DrawPolygon:
                {

                    QPointF point = glPressPos().toPointF() - offset;
                    if ( event.modifiers() & Qt::ShiftModifier )
                        point = adjust(point);

                    currentGeometry->append(point);
                    if ( currentGeometry->count() == 1 )
                        currentGeometry->append(point);

                }

                break;

            case DrawCircle:
                mode = None;
                break;

            }

        } else {

            if ( mode == DrawPolygon )
                p_scene->updateGeometry();

            mode = None;

        }

    }
    void mouseMoved( const QyGL::MouseEvent & event ) {

        if ( !isMousePressed() )
            return;

        bool needAdjust = event.modifiers() & Qt::ShiftModifier;

        if ( mode == None )
            return;
        else switch (mode) {

        case Analyse:
            mode = DrawCircle;
            if ( !geometry->isCircle() && geometry->isEmpty() ) {

                currentGeometry = geometry;
                offset = QPointF();

            } else {

                QpwWorld::Geometry newGeo;
                geometry->subGeometry.append(newGeo);
                currentGeometry = &geometry->subGeometry.last();
                offset = geometry->center;

            }

            if ( currentGeometry->isNull() ) {

                currentGeometry->center = glPressPos().toPointF() - offset;

                if (needAdjust)
                    currentGeometry->center = adjust(currentGeometry->center);

            }

        case DrawCircle:
            currentGeometry->setRadius( needAdjust ? adjust( glPressOffset().length() )
                : glPressOffset().length() );
            break;

        case DrawPolygon:

            currentGeometry->last() = needAdjust
                ? adjust( event.glPos().toPointF() - offset )
                : event.glPos().toPointF() - offset;

            break;

        case None:
            break;

        }

    }
    void mouseReleased( const QyGL::MouseEvent & event ) {

        bool needAdjust = event.modifiers() & Qt::ShiftModifier;

        if ( mode == None ) {

            return;
        } else switch (mode) {

        case Analyse:
            mode = DrawPolygon;
            if ( !geometry->isCircle() && geometry->isEmpty() ) {

                currentGeometry = geometry;
                offset = QPointF();

            } else {

                QpwWorld::Geometry newGeo;
                geometry->subGeometry.append(newGeo);
                offset = geometry->center;
                currentGeometry = &geometry->subGeometry.last();

            }

            {
                QPointF point = needAdjust
                    ? adjust( glPressPos().toPointF() - offset )
                    : glPressPos().toPointF() - offset;

                currentGeometry->append(point);

            }
            offset += currentGeometry->center;

        case DrawPolygon:
            break;

        case DrawCircle:
            mode = None;
            p_scene->updateGeometry();
            break;

        case None:
            break;

        }

    }

//     void keyPressd( QKeyEvent * event ) {

//         if ( event->key() == Qt::Key_Shift )
//             p_scene->adjustCursor = true;

//     }
//     void keyReleased( QKeyEvent * event ) {

//         if ( event->key() == Qt::Key_Shift )
//             p_scene->adjustCursor = false;

//     }

    QpwGeometryScene * p_scene;
    Mode mode;
    QpwWorld::Geometry * geometry,
        * currentGeometry;
    QPointF offset;

};

/**************************************************************************************************/

QpwGeometryEdit::QpwGeometryEdit( QyGL::ViewWidget * viewWidget )
    : QObject(viewWidget), p_viewWidget(viewWidget)
{

    p_geometryScene = new QpwGeometryScene;
//     ( new QyGL::BasicCamViewController(p_geometryScene) )->setEnabled(true);

    if (p_viewWidget) {

        QyTexturePool::staticPool()->glWidget = p_viewWidget->glWidget();
        p_viewWidget->setScene(p_geometryScene);

    }

    DrawTool * drawTool = new DrawTool(p_geometryScene);

    drawTool->setEnabled(true);

}

QpwGeometryEdit::~QpwGeometryEdit() {
}

QpwWorld::Geometry QpwGeometryEdit::geometry() const {

    return p_geometryScene->geometry();
}

void QpwGeometryEdit::setGeometry( const QpwWorld::Geometry & geometry ) {

    p_geometryScene->setGeometry(geometry);

}

void QpwGeometryEdit::clear() {

    QpwWorld::Geometry geo = geometry();

    geo.clear();

    setGeometry(geo);

}
